﻿/*
 * DHT Node
 * 
 * Author: snowdreamis
 * Date: 2011/11/22
 * 
 */

using System;
using System.Collections.Generic;
using System.Net;
using System.Linq;
using System.Text;
using BitTorrentSharp.Maths;

namespace BitTorrentSharp.DHT
{
    /// <summary>
    /// NodeID
    /// </summary>
    public sealed class NodeID : IEquatable<NodeID>, IComparable<NodeID>, IComparable
    {
        #region static
        /// <summary>
        /// Calculate the distance between two ids
        /// </summary>
        /// <param name="id1">id1</param>
        /// <param name="id2">id2</param>
        /// <returns>The distance</returns>
        public static BigInteger CalculateDistance(NodeID id1, NodeID id2)
        {
            if (id1 == null)
                throw new ArgumentNullException("id1");
            if (id2 == null)
                throw new ArgumentNullException("id2");

            return id1.BigNumber ^ id2.BigNumber;
        }
        /// <summary>
        /// Calculare the distance between a node id and info hash
        /// </summary>
        /// <param name="id"></param>
        /// <param name="infoHash"></param>
        /// <returns></returns>
        public static BigInteger CalculateDistance(NodeID id, byte[] infoHash)
        {
            if (id == null)
                throw new ArgumentNullException("id");
            if (infoHash == null)
                throw new ArgumentNullException("infoHash");
            if (infoHash.Length != 20)
                throw new ArgumentException("infoHash's length must be 20");

            return id.BigNumber ^ new BigInteger(infoHash);
        }
        /// <summary>
        /// - operate, calculate distance
        /// </summary>
        public static BigInteger operator -(NodeID id1, NodeID id2)
        {
            return CalculateDistance(id1, id2);
        }

        #endregion

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="id">id in bytes, 160bits</param>
        public NodeID(byte[] id)
        {
            if (id == null)
                throw new ArgumentNullException("id");
            if (id.Length != 20)
                throw new ArgumentException("id's length must be 20");
            // create big number
            this.BigNumber = new BigInteger(id);
        }

        #region internal
        /// <summary>
        /// big number
        /// </summary>
        internal BigInteger BigNumber { get; private set; }

        #endregion
        /// <summary>
        /// Get Distance
        /// </summary>
        public BigInteger GetDistanceTo(NodeID nodeID)
        {
            return NodeID.CalculateDistance(this,nodeID);
        }
        /// <summary>
        /// Get Distance
        /// </summary>
        public BigInteger GetDistanceTo(byte[] infoHash)
        {
            return NodeID.CalculateDistance(this, infoHash);
        }

        #region compare

        public int CompareTo(object obj)
        {
            if (obj is NodeID)
                return this.CompareTo(obj as NodeID);
            else
                throw new ArgumentException(string.Format("Cannot compare to type {0}", obj.GetType().Name));
        }

        public int CompareTo(NodeID other)
        {
            return this.BigNumber.CompareTo(other.BigNumber);
        }
        #endregion

        #region Equal and hash
        
        public override int GetHashCode()
        {
            return this.BigNumber.GetHashCode();
        }

        public override bool Equals(object obj)
        {
            if (obj == null)
                return false;
            if (obj is NodeID)
                return this.Equals(obj as NodeID);
            else
                return false;
        }

        public bool Equals(NodeID other)
        {
            if (other == null)
                return false;
            return this.BigNumber.Equals(other.BigNumber);
        }

        #endregion
    }

    /// <summary>
    /// node state
    /// </summary>
    public enum NodeState
    {
        Unknown,
        Good,
        Questionable,
        Bad
    }

    /// <summary>
    /// DHT Node
    /// </summary>
    public sealed class Node: INodeChangedNotify
    {
        #region static
        /// <summary>
        /// Get distance between two node
        /// </summary>
        /// <param name="node1"></param>
        /// <param name="node2"></param>
        /// <returns></returns>
        public static BigInteger operator-(Node node1, Node node2)
        {
            if(node1 == null)
                throw new ArgumentNullException("node1");
            if(node2 == null)
                throw new ArgumentNullException("node2");

            return node1.ID - node2.ID;
        }

        #endregion
        /// <summary>
        /// Constructor, internal
        /// </summary>
        /// <param name="nodeID">node id</param>
        internal Node(
            DHTEngine engine, 
            NodeID nodeID, 
            IPEndPoint endPoint)
        {
            if (engine == null)
                throw new ArgumentNullException("engine");
            if (nodeID == null)
                throw new ArgumentNullException("nodeID");
            if (endPoint == null)
                throw new ArgumentNullException("endPoint");

            this.Engine = engine;
            this.ID = nodeID;
            this.EndPoint = endPoint;
            this.LastActiveTime = DateTime.MinValue;
            this.Token = null;
        }

        #region private
        /// <summary>
        /// Is this node initialized
        /// </summary>
        private bool _isInitialized = false;
        /// <summary>
        /// Is this node a known bad node
        /// </summary>
        private bool _isKnownBad = false;

        #endregion

        #region properties
        /// <summary>
        /// Engine
        /// </summary>
        public DHTEngine Engine { get; private set; }
        /// <summary>
        /// Node id
        /// </summary>
        public NodeID ID { get; private set; }
        /// <summary>
        /// end point
        /// </summary>
        public IPEndPoint EndPoint { get; private set; }
        /// <summary>
        /// last active time
        /// </summary>
        public DateTime LastActiveTime { get; private set; }
        /// <summary>
        /// State
        /// </summary>
        public NodeState State
        {
            get
            {
                if (!this._isInitialized)
                {
                    return NodeState.Unknown;
                }
                else
                {
                    if (this._isKnownBad)
                        return NodeState.Bad;
                    else if (DateTime.Now - this.LastActiveTime < this.Engine.Configuration.Routing.Node.TimeOut)
                        return NodeState.Good;
                    else
                        return NodeState.Questionable;
                }
            }
        }
        /// <summary>
        /// Token
        /// </summary>
        internal byte[] Token { get; private set; }

        #endregion

        #region basic operation

        #region ping
        /// <summary>
        /// Ping this node synchronously
        /// This method will raise Pinged event
        /// </summary>
        /// <returns></returns>
        public bool Ping()
        {
            throw new NotImplementedException();
        }
        /// <summary>
        /// Ping this node asynchronously
        /// Since this method will call Ping() as last, so it'll raise Pinged event
        /// </summary>
        public IAsyncResult BeginPing(AsyncCallback callback, object state)
        {
            Func<bool> pingFunc = new Func<bool>(this.Ping);
            return pingFunc.BeginInvoke(callback, state);
        }
        /// <summary>
        /// End asynchronously ping
        /// </summary>
        /// <param name="result"></param>
        /// <returns></returns>
        public bool EndPing(IAsyncResult result)
        {
            Func<bool> pingFunc = new Func<bool>(this.Ping);
            return pingFunc.EndInvoke(result);
        }
        #endregion

        #endregion

        #region protected
        /// <summary>
        /// When node state changed
        /// </summary>
        protected void OnNodeStateChanged(NodeState oldState, NodeState newState)
        {
            if (this.NodeStateChanged != null)
                this.NodeStateChanged(this, new NodeStateChangedEventArgs(this, oldState, newState));
        }

        #endregion

        #region INodeStateChanged
        /// <summary>
        /// Raise when node state changed
        /// </summary>
        public event EventHandler<NodeStateChangedEventArgs> NodeStateChanged;
        /// <summary>
        /// Raise when a ping action done
        /// </summary>
        public event EventHandler<PingedEventArgs> Pinged;

        #endregion
    }
}
